Windpark Kelmarsh (2016) - 12.5 MW

Reliability Evaluation

Author

Jorge A. Thomas

Published

April 1, 2025

Project Overview

“Kelmarsh Wind Farm is located near Haselbach, Northamptonshire and comprises six 2.05MW Senvion MM92 turbines. The Project was acquired from EON in December 2014 and was constructed in joint venture with Santander using balance sheet finance. Construction was completed in April 2016. It is now owned by Cubico Investments.”

  • https://www.blue-energyco.com/our-projects/kelmarsh/

Kelmarsh Wind Farm

Senvion MM92 turbine
Figure 1: Near Haselbach, Northamptonshire (UK).

Read parquet files

Show the code
```{python}
#| label: Read-Data
tstatus = pl.read_parquet(f"{folder_path_interim}Kelmarsh_Turbines_Status_20160114_20230109_n385133_cols12.parquet")

tstatus
```
shape: (385_133, 12)
Timestamp start Timestamp end Status Code Message Comment Service contract category IEC category wt Global contract category Custom contract category Duration
datetime[μs, UTC] datetime[μs, UTC] str i64 str str str str str str str duration[μs]
2016-01-14 19:28:03 UTC 2016-01-23 14:36:32 UTC "Stop" 111 "Emergency stop nacelle" null "Emergency stop switch (Nacelle… "Forced outage" "T01" null null 8d 19h 8m 29s
2016-01-14 19:28:03 UTC 2016-01-14 19:38:03 UTC "Warning" 5720 "Brake accumulator defect" null "Warnings (27)" null "T01" null null 10m
2016-01-14 19:28:05 UTC 2016-01-23 11:27:46 UTC "Informational" 3835 "Cable panel breaker open" null "Warnings (27)" null "T01" null null 8d 15h 59m 41s
2016-01-14 19:28:05 UTC 2016-01-23 11:27:46 UTC "Informational" 3830 "Supply circuit breaker earthed" null "Warnings (27)" "Full Performance" "T01" null null 8d 15h 59m 41s
2016-01-14 19:28:05 UTC 2016-01-23 14:09:18 UTC "Warning" 3870 "Overload transformer fan outle… null "Warnings (27)" "Full Performance" "T01" null null 8d 18h 41m 13s
2022-12-31 12:28:30 UTC null "Informational" 100160 "System test 3" null null "Technical Standby" "T06" null null null
2022-12-31 12:28:45 UTC null "Informational" 100180 "Run-up" null null "Technical Standby" "T06" null null null
2022-12-31 12:30:58 UTC null "Informational" 100190 "Mains connection" null null "Full Performance" "T06" null null null
2022-12-31 12:31:08 UTC null "Informational" 100200 "Mains run-up" null null "Full Performance" "T06" null null null
2022-12-31 12:31:38 UTC null "Informational" 100210 "Mains operation" null null "Full Performance" "T06" null null null

Filter Stop

Show the code
```{python}
#| label: Filter-Stops

stops = pl.sql(    
    """
    SELECT * EXCLUDE ("Custom contract category")    
    FROM tstatus
    WHERE "Status" = 'Stop'
    """).collect()
```

Filter Forced Outages

Show the code
```{python}
#| label: Filter-Forced-Outages

# Aggregate tables using SQL syntax
foutages = pl.sql(
    """
    SELECT * 
    FROM stops
    WHERE "IEC category" = 'Forced outage' 
    """).collect()

foutages = foutages.rename({"Duration":"TTR"})

# TTF
foutages = foutages.with_columns(
    (pl.col("Timestamp start").shift(-1) - pl.col("Timestamp end")).shift(1).alias("TTF")
    )

foutages = foutages.with_columns(
    pl.when(pl.col("TTF") < 0).then(None).otherwise(pl.col("TTF")).alias("TTF")
)

foutages
```
shape: (892, 12)
Timestamp start Timestamp end Status Code Message Comment Service contract category IEC category wt Global contract category TTR TTF
datetime[μs, UTC] datetime[μs, UTC] str i64 str str str str str str duration[μs] duration[μs]
2016-01-14 19:28:03 UTC 2016-01-23 14:36:32 UTC "Stop" 111 "Emergency stop nacelle" null "Emergency stop switch (Nacelle… "Forced outage" "T01" null 8d 19h 8m 29s null
2016-01-23 15:05:30 UTC 2016-01-23 15:06:42 UTC "Stop" 117 "Emergency stop base box" null "Emergency stop switch (Convert… "Forced outage" "T01" null 1m 12s 28m 58s
2016-01-24 16:51:17 UTC 2016-02-01 19:46:41 UTC "Stop" 3110 "Frequency converter error" null "Generator and Converter errors… "Forced outage" "T01" null 8d 2h 55m 24s 1d 1h 44m 35s
2016-02-11 14:36:37 UTC 2016-02-11 15:14:50 UTC "Stop" 3585 "Maximum grid frequency" null "External stop (grid) (4)" "Forced outage" "T01" null 38m 13s 9d 18h 49m 56s
2016-03-01 17:33:14 UTC 2016-03-01 17:35:14 UTC "Stop" 3000 "Frequency converter not ready" null "Generator and Converter errors… "Forced outage" "T01" null 2m 19d 2h 18m 24s
2022-05-11 14:08:43 UTC 2022-05-11 14:31:27 UTC "Stop" 555 "Pitch angle deviation" null "Pitch errors (18)" "Forced outage" "T06" "09 (Int) Fault" 22m 44s 2h 55m 14s
2022-05-12 02:37:43 UTC 2022-05-19 11:19:38 UTC "Stop" 555 "Pitch angle deviation" "Pitch fault - techs attended o… "Pitch errors (18)" "Forced outage" "T06" "09 (Int) Fault" 7d 8h 41m 55s 12h 6m 16s
2022-07-11 08:13:14 UTC 2022-07-11 13:48:21 UTC "Stop" 6530 "Anemometer defect" "Anemometer failed and replaced" "Sensor error (21)" "Forced outage" "T06" "09 (Int) Fault" 5h 35m 7s 52d 20h 53m 36s
2022-07-19 13:17:02 UTC 2022-07-19 18:11:27 UTC "Stop" 3151 "Max.temp.conv.inl.>perm.out.t." "Converters overheating due to … "Temperature error (22)" "Forced outage" "T06" "09 (Int) Fault" 4h 54m 25s 7d 23h 28m 41s
2022-11-02 17:39:07 UTC 2022-11-02 18:25:51 UTC "Stop" 21 "Manual stop - remote" null "Remote stop (30)" "Forced outage" "T06" "09 (Int) Fault" 46m 44s 105d 23h 27m 40s

Time Series for Binary Plot

Show the code
```{python}
#| label: Binary-Plot

dtstarts = foutages.select(["Timestamp start", "Status", "Message",  "wt"]).rename({"Timestamp start": "Timestamp"})
dtends = foutages.select(["Timestamp end", "Status", "Message",  "wt"]).with_columns(pl.lit("Start").alias("Status")).rename({"Timestamp end": "Timestamp"})

tstates = pl.concat([dtstarts, dtends]).sort(["wt", 'Timestamp'])

# tstates_ts = dtstarts.join(dtends, on="Timestamp", how="full").sort(["wt", 'Timestamp'])

tstates= tstates.with_columns(
    pl.when(pl.col("Status") == "Stop").then(0).otherwise(1).alias("State"),
    pl.col("Timestamp").dt.year().alias("Year")    
    )

tstates= tstates.with_columns(    
    (pl.col("wt") + "-" + pl.col("Year").cast(str)).alias("wt_Year")
    )

tstates
#TODO: remove missing years!
```
shape: (1_784, 7)
Timestamp Status Message wt State Year wt_Year
datetime[μs, UTC] str str str i32 i32 str
2016-01-14 19:28:03 UTC "Stop" "Emergency stop nacelle" "T01" 0 2016 "T01-2016"
2016-01-23 14:36:32 UTC "Start" "Emergency stop nacelle" "T01" 1 2016 "T01-2016"
2016-01-23 15:05:30 UTC "Stop" "Emergency stop base box" "T01" 0 2016 "T01-2016"
2016-01-23 15:06:42 UTC "Start" "Emergency stop base box" "T01" 1 2016 "T01-2016"
2016-01-24 16:51:17 UTC "Stop" "Frequency converter error" "T01" 0 2016 "T01-2016"
2022-07-11 13:48:21 UTC "Start" "Anemometer defect" "T06" 1 2022 "T06-2022"
2022-07-19 13:17:02 UTC "Stop" "Max.temp.conv.inl.>perm.out.t." "T06" 0 2022 "T06-2022"
2022-07-19 18:11:27 UTC "Start" "Max.temp.conv.inl.>perm.out.t." "T06" 1 2022 "T06-2022"
2022-11-02 17:39:07 UTC "Stop" "Manual stop - remote" "T06" 0 2022 "T06-2022"
2022-11-02 18:25:51 UTC "Start" "Manual stop - remote" "T06" 1 2022 "T06-2022"
Show the code
```{python}
#| label: Find-Missing-Values

# Get rows containing any null values
missing = (tstates
          .with_row_count("row_index")
          .filter(pl.any_horizontal(pl.all().is_null()))
          .sort("row_index"))

# Display rows with missing values
print("Rows containing missing values:")
print(missing)

# Get count of missing values per column
print("\nMissing value counts per column:")
print(tstates.null_count())
```
Rows containing missing values:
shape: (5, 8)
┌───────────┬───────────────────┬────────┬────────────────────────┬─────┬───────┬──────┬─────────┐
│ row_index ┆ Timestamp         ┆ Status ┆ Message                ┆ wt  ┆ State ┆ Year ┆ wt_Year │
│ ---       ┆ ---               ┆ ---    ┆ ---                    ┆ --- ┆ ---   ┆ ---  ┆ ---     │
│ u32       ┆ datetime[μs, UTC] ┆ str    ┆ str                    ┆ str ┆ i32   ┆ i32  ┆ str     │
╞═══════════╪═══════════════════╪════════╪════════════════════════╪═════╪═══════╪══════╪═════════╡
│ 426       ┆ null              ┆ Start  ┆ Emergency stop top box ┆ T02 ┆ 1     ┆ null ┆ null    │
│ 427       ┆ null              ┆ Start  ┆ Safety chain open      ┆ T02 ┆ 1     ┆ null ┆ null    │
│ 428       ┆ null              ┆ Start  ┆ Emergency stop nacelle ┆ T02 ┆ 1     ┆ null ┆ null    │
│ 786       ┆ null              ┆ Start  ┆ Emergency stop top box ┆ T03 ┆ 1     ┆ null ┆ null    │
│ 1486      ┆ null              ┆ Start  ┆ Overload gear oil pump ┆ T06 ┆ 1     ┆ null ┆ null    │
└───────────┴───────────────────┴────────┴────────────────────────┴─────┴───────┴──────┴─────────┘

Missing value counts per column:
shape: (1, 7)
┌───────────┬────────┬─────────┬─────┬───────┬──────┬─────────┐
│ Timestamp ┆ Status ┆ Message ┆ wt  ┆ State ┆ Year ┆ wt_Year │
│ ---       ┆ ---    ┆ ---     ┆ --- ┆ ---   ┆ ---  ┆ ---     │
│ u32       ┆ u32    ┆ u32     ┆ u32 ┆ u32   ┆ u32  ┆ u32     │
╞═══════════╪════════╪═════════╪═════╪═══════╪══════╪═════════╡
│ 5         ┆ 0      ┆ 0       ┆ 0   ┆ 0     ┆ 5    ┆ 5       │
└───────────┴────────┴─────────┴─────┴───────┴──────┴─────────┘
C:\Users\SAL9000\AppData\Local\Temp\ipykernel_20864\4106017410.py:3: DeprecationWarning:

`DataFrame.with_row_count` is deprecated. Use `with_row_index` instead. Note that the default column name has changed from 'row_nr' to 'index'.

Plot Square Signal

Show the code
```{python}
#| label: Square-Signal-Plot

import plotly.express as px

# Other line_shape options, or interpolation methods between given points:

# 'hv' step ends, equivalent to pyplot's post option;
# 'vh' step starts;
# 'hvh' step middles, x axis;
# 'vhv' step middles, y axis;
# 'spline' smooth curve between points;
# 'linear' line segments between points, default value for line_shape.

fig = px.line(tstates, x='Timestamp', y="State", line_shape='hv', facet_row="wt_Year", color="wt",
     title="Time / State Diagram for Kellmarsh Wind Turbines (2016-2022)",
     subtitle= "(0) Down Step=Forced outage (IEC), (1) Up Step=Start",
     labels={"Timestamp": "Time (UTC)", "State": "", "wt": ""},facet_row_spacing=0.01 )

# Update y-axis to show only 0 and 1 as labels
fig.update_yaxes(
    tickvals=[0, 1],  # Set tick values to 0 and 1
    ticktext=["", ""]  # Optional: Explicitly set tick text
)

fig.update_xaxes(matches=None, tickformat="%b"  )


# Update facet labels orientation and size
fig.for_each_annotation(lambda a: a.update(text=a.text.split("-")[-1],textangle=0, xanchor='left', x=-0.04))  # Make labels horizontal
fig.update_annotations(font_size=8)  # Reduce font size

fig.show()
```